<?php


namespace EchoPHP\Foundation\Exception;

use EchoPHP\Facades\View;
use EchoPHP\Http\Exception\HttpResponseException;
use EchoPHP\Http\Response\JsonResponse;
use EchoPHP\Http\Response;
use EchoPHP\Support\Contacts\Responsable;
use EchoPHP\Foundation\Exception\Contacts\HttpException as HttpExceptionContact;
use Exception;
use EchoPHP\Foundation\Container;
use EchoPHP\Support\Contacts\Debug\ExceptionHandler as ExceptionHandlerContract;
use Symfony\Component\Console\Application as ConsoleApplication;
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\Debug\ExceptionHandler as SymfonyExceptionHandler;
use Whoops\Run as Whoops;

class Handler implements ExceptionHandlerContract {

    /**
     * 容器实现
     * @var
     */
    protected $container;

    /**
     * 不报告的异常类型列表
     * @var array
     */
    protected $dontReport = [
        HttpResponseException::class,
        HttpException::class,
        \BadMethodCallException::class
    ];

    /**
     * 不报告的内置异常类型列表系统级异常,此类异常不会写入日志文件
     * @var array
     */
    protected $internalDotReport = [
    ];

    /**
     * 提交参数中自动过滤不予显示的集合，如密码和确认密码字段，防止数据泄露
     * @var array
     */
    protected $dotFlash = [
        'password', 'password_confirm'
    ];

    public function __construct(Container $container) {
        $this->container = $container;
    }

    /**
     * 报告或者使用日志记录异常信息,报告本身不会中断程序的执行
     * @param Exception $e
     * @return mixed|void
     * @throws Exception
     */
    public function report(Exception $e) {
        if (!$this->shouldReport($e)) {
            return;
        }
        try {
            //异常处理注册之前已经注册了日志分析程序,所以可以直接构建日志实例
            $logger = $this->container->make('log');
        } catch (Exception $ex) {
            throw $e;
        }

        $logger->error(
            $e->getMessage(),
            array_merge($this->context(), ['exception' => $e])
        );
    }

    /**
     * 判断异常是否需要报告出来
     * @param Exception $e
     * @return bool|mixed
     */
    public function shouldReport(Exception $e) {
        $dotReport = array_merge($this->dontReport, $this->internalDotReport);

        foreach ($dotReport as $exceptionType) {
            if ($e instanceof $exceptionType) {
                return false;
            }
        }
        return true;
    }

    /**
     * 获取默认的上下文内容
     * @return array
     */
    protected function context() {
        return [];
    }

    /**
     * 渲染输出异常信息
     * @param \EchoPHP\Http\Request $request
     * @param Exception $e
     * @return JsonResponse|\EchoPHP\Http\Response|mixed
     * @throws Exception
     */
    public function render($request, Exception $e) {
        if (method_exists($e, 'render') && $response = $e->render($request)) {
            return $response;
        } elseif ($e instanceof Responsable) {
            return $e->toResponse($request);
        }
        $e = $this->prepareException($e);

        if ($e instanceof HttpResponseException) {
            return $e->getResponse();
        }

        return $request->expectsJson() ?
            $this->prepareJsonResponse($e) :
            $this->prepareResponse($e);

    }

    /**
     * 生成json格式的响应
     * @param Exception $e
     * @return JsonResponse
     * @throws Exception
     */
    protected function prepareJsonResponse(Exception $e) {
        return new JsonResponse(
            $this->convertExceptionToArray($e),
            $this->isHttpException($e) ? $e->getStatusCode() : 500,
            $this->isHttpException($e) ? $e->getHeaders() : [],
        );
    }

    /**
     * 将异常信息转换为数组
     * @param Exception $e
     * @return array
     */
    protected function convertExceptionToArray(Exception $e) {
        if (!env('APP_DEBUG', false)) {
            return [
                'message' => $this->isHttpException($e) ? $e->getMessage() : '服务器出现错误',
            ];
        } else {
            $traceMessage = [
                'message' => $e->getMessage(),
            ];
            $traceMessage['trace'][] = [
                'file' => $e->getFile(),
                'line' => $e->getLine(),
                'class' => get_class($e),
                'function' => '',
            ];
            foreach ($e->getTrace() as $trace) {
                $traceMessage['trace'][] = [
                    'file' => $trace['file'] ?? '',
                    'line' => $trace['line'] ?? '',
                    'class' => $trace['class'] ?? '',
                    'function' => $trace['function'] ?? '',
                ];
            }

            return $traceMessage;
        }
    }

    /**
     * 生成普通http响应
     * @param Exception $e
     * @return Response
     * @throws Exception
     */
    protected function prepareResponse(Exception $e) {
        //如果不是http异常，但是打开了app调试，那么也直接将结果渲染出来
        if (!$this->isHttpException($e) && env('APP_DEBUG', false)) {
            return $this->convertExceptionToResponse($e);
        }
        //如果没开启app调试，且不是http异常，那么转为http异常，后续使用http异常渲染程序处理
        if (!$this->isHttpException($e)) {
            $e = new HttpException(500, $e->getMessage());
        }
        return $this->renderHttpException($e);
    }

    /**
     * 渲染http异常
     * @param Exception $e
     * @return Response
     * @throws Exception
     */
    protected function renderHttpException(Exception $e) {
        $this->registerErrorViewPath();

        if (View::exists($view = "errors::{$e->getStatusCode()}")) {
            return Response::create(View::render($view, ['code' => $e->getStatusCode(), 'message' => $e->getMessage()]),
                $this->isHttpException($e) ? $e->getStatusCode() : 500,
                $this->isHttpException($e) ? $e->getHeaders() : [],
                );
        } elseif (View::exists($view = "errors::default")) {
            return Response::create(View::render($view, ['code' => $e->getStatusCode(), 'message' => $e->getMessage()]),
                $this->isHttpException($e) ? $e->getStatusCode() : 500,
                $this->isHttpException($e) ? $e->getHeaders() : [],
                );
        }

        return $this->convertExceptionToResponse($e);
    }

    /**
     * 添加视图错误日志路径
     * @throws Exception
     */
    protected function registerErrorViewPath() {
        $viewPath = $this->container->make('path.view') . '/errors';
        if (!is_dir($viewPath)) {
            $viewPath = __DIR__ . '/views';
        }
        View::addFolder('errors', $viewPath);
    }

    /**
     * 将异常转换为响应
     * @param Exception|HttpExceptionContact $e
     * @return Response
     */
    protected function convertExceptionToResponse(Exception $e) {
        return Response::create(
            $this->renderExceptionContent($e),
            $this->isHttpException($e) ? $e->getStatusCode() : 500,
            $this->isHttpException($e) ? $e->getHeaders() : [],
            );
    }

    /**
     * 将异常渲染成html
     * @param Exception $e
     * @return string
     */
    protected function renderExceptionContent(Exception $e) {
        try {
            if (env('APP_DEBUG', false)) {
                return $this->renderExceptionWithWhoops($e);
            } else {
                return $this->renderExceptionWithSymfony($e, env('APP_DEBUG', false));
            }
        } catch (Exception $e) {
            return $this->renderExceptionWithSymfony($e, env('APP_DEBUG', false));
        }
    }

    /**
     * 使用Whoops来渲染异常输出
     * @param Exception $e
     * @return string
     */
    protected function renderExceptionWithWhoops(Exception $e) {
        $whoops = new Whoops();
        $whoops->prependHandler((new WhoopsHandler())->forDebug());
        $whoops->writeToOutput(false);
        $whoops->allowQuit(false);
        return $whoops->handleException($e);
    }

    /**
     * 使用symfony来渲染异常输出
     * @param Exception $e
     * @param $debug
     * @return string
     */
    protected function renderExceptionWithSymfony(Exception $e, $debug) {
        return (new SymfonyExceptionHandler($debug))->getHtml(
            FlattenException::create($e)
        );
    }

    /**
     * 根据传入的异常类型，转换层一个用于渲染到页面的异常
     * @param Exception $e
     * @return Exception
     */
    protected function prepareException(Exception $e) {
        return $e;
    }

    /**
     * 将错误信息输出到命令行界面，直接输出真实错误原因
     * @param $output
     * @param Exception $e
     * @return mixed|void
     */
    public function renderForConsole($output, Exception $e) {
        (new ConsoleApplication())->renderException($e, $output);
    }

    /**
     * 判断是否是HTTP异常
     * @param Exception $e
     * @return bool
     */
    public function isHttpException(Exception $e) {
        return $e instanceof HttpExceptionContact;
    }
}